package com.googlecode.openrtp.util.serviceloader;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.util.*;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.StandardLocation;

/**
 *
 * @author jocke
 */
@SupportedAnnotationTypes("com.googlecode.openrtp.util.serviceloader.Service")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class ServiceProcessor extends AbstractProcessor {

    private static final Map<String, List<String>> REGISTRATIONS = new WeakHashMap<String, List<String>>();
    private static final String DEFAULT_PKG = "";

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (roundEnv.errorRaised()) {
            return false;

        } else if (!roundEnv.processingOver()) {
            registerServices(roundEnv);
            return false;

        } else {
            printServices();
            REGISTRATIONS.clear();
            return true;
        }

    }

    private void registerServices(RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(Service.class)) {
            if (isValidService(element)) {
                List<String> clazzes = getClassesFromAnnotation(findAnnotationMirror(element, Service.class).getElementValues());
                Name binaryName = processingEnv.getElementUtils().getBinaryName((TypeElement) element);
                register(binaryName.toString(), clazzes);
            }
        }
    }

    private boolean isValidService(Element element) {
        if (!element.getModifiers().contains(Modifier.PUBLIC)) {
            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "A service must be a public class");
            return false;
        } else if (element.getModifiers().contains(Modifier.ABSTRACT)) {
            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "A service can not be abstract");
            return false;
        } else if (!hasDefaultConstructor(element)) {
            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "A service has to have a public no argument default constructor");
            return false;
        }

        return true;
    }

    private boolean hasDefaultConstructor(Element element) {
        for (ExecutableElement constructor : ElementFilter.constructorsIn(element.getEnclosedElements())) {
            if (constructor.getModifiers().contains(Modifier.PUBLIC) && constructor.getParameters().isEmpty()) {
                return true;
            }
        }
        return false;
    }

    private AnnotationMirror findAnnotationMirror(Element element, Class<? extends Annotation> annotation) {
        for (AnnotationMirror ann : element.getAnnotationMirrors()) {
            if (processingEnv.getElementUtils().getBinaryName((TypeElement) ann.getAnnotationType().asElement()).contentEquals(annotation.getName())) {
                return ann;
            }
        }
        throw new AssertionError("No service annotation found! should not happen");
    }

    private List<String> getClassesFromAnnotation(Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues) {
        List<String> clazzes = new ArrayList<String>();
        for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : elementValues.entrySet()) {
            if (entry.getKey().getSimpleName().toString().equals("value")) {
                List< ? extends AnnotationValue> value = (List) entry.getValue().getValue();
                for (AnnotationValue annotationValue : value) {
                    clazzes.add(annotationValue.getValue().toString());
                }
            }
        }
        return clazzes;
    }

    private void register(String service, List<String> classes) {
        for (String clazz : classes) {
            putIfAbsent(clazz);
            REGISTRATIONS.get(clazz).add(service);
        }
    }

    private void putIfAbsent(String clazz) {
        if (!REGISTRATIONS.containsKey(clazz)) {
            REGISTRATIONS.put(clazz, new ArrayList<String>());
        }
    }

    private void printServices() {
        for (Map.Entry<String, List<String>> entry : REGISTRATIONS.entrySet()) {
            try {
                FileObject fileObject = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, DEFAULT_PKG, "META-INF/services/" + entry.getKey(), new Element[0]);
                OutputStream os = fileObject.openOutputStream();
                try {
                    PrintWriter w = new PrintWriter(new OutputStreamWriter(os, "UTF-8"));
                    for (String line : entry.getValue()) {
                        w.println(line);
                    }
                    w.flush();
                    w.close();
                } finally {
                    os.close();
                }
            } catch (IOException x) {
                processingEnv.getMessager().printMessage(Kind.ERROR, "Failed to write to " + entry.getKey() + ": " + x.toString());
            }
        }
    }

    /*
     * Only used for test
     */
    static Map<String, List<String>> getRegistrations() {
        return REGISTRATIONS;
    }
}
